/****************************************************************************
 * sched/task/task_exithook.c
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <assert.h>
#include <debug.h>
#include <errno.h>

#include <nuttx/sched.h>
#include <nuttx/fs/fs.h>
#include <nuttx/mm/mm.h>

#include "sched/sched.h"
#include "group/group.h"
#include "signal/signal.h"
#include "task/task.h"

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name: nxtask_exitstatus
 *
 * Description:
 *   Report exit status when main task of a task group exits
 *
 ****************************************************************************/

#ifdef CONFIG_SCHED_CHILD_STATUS
static inline void nxtask_exitstatus(FAR struct task_group_s *group,
                                     int status)
{
  FAR struct child_status_s *child;

  /* Check if the parent task group has suppressed retention of
   * child exit status information.
   */

  if ((group->tg_flags & GROUP_FLAG_NOCLDWAIT) == 0)
    {
      /* No.. Find the exit status entry for this task in the parent TCB */

      child = group_find_child(group, nxsched_getpid());
      if (child)
        {
          /* Save the exit status..  For the case of HAVE_GROUP_MEMBERS,
           * the child status will be as exited until the last member
           * of the task group exits.
           */

          child->ch_status = status;
        }
    }

  group->tg_exitcode = status;
}
#else

#  define nxtask_exitstatus(group,status) (group)->tg_exitcode = (status);

#endif /* CONFIG_SCHED_CHILD_STATUS */

/****************************************************************************
 * Name: nxtask_groupexit
 *
 * Description:
 *   Mark that the final thread of a child task group as exited.
 *
 ****************************************************************************/

#ifdef CONFIG_SCHED_CHILD_STATUS
static inline void nxtask_groupexit(FAR struct task_group_s *group)
{
  FAR struct child_status_s *child;

  /* Check if the parent task group has suppressed retention of child exit
   * status information.
   */

  if ((group->tg_flags & GROUP_FLAG_NOCLDWAIT) == 0)
    {
      /* No.. Find the exit status entry for this task in the parent TCB */

      child = group_find_child(group, nxsched_getpid());
      if (child)
        {
          /* Mark that all members of the child task group has exited */

          child->ch_flags |= CHILD_FLAG_EXITED;
        }
    }
}

#else

#  define nxtask_groupexit(group)

#endif /* CONFIG_SCHED_CHILD_STATUS */

/****************************************************************************
 * Name: nxtask_sigchild
 *
 * Description:
 *   Send the SIGCHLD signal to the parent thread
 *
 ****************************************************************************/

#ifdef CONFIG_SCHED_HAVE_PARENT
#ifdef HAVE_GROUP_MEMBERS
static inline void nxtask_sigchild(pid_t ppid, FAR struct tcb_s *ctcb,
                                   int status)
{
  FAR struct task_group_s *chgrp = ctcb->group;
  FAR struct task_group_s *pgrp;
  siginfo_t info;

  DEBUGASSERT(chgrp);

  /* Get the parent task group.  It is possible that all of the members of
   * the parent task group have exited.  This would not be an error.  In
   * this case, the child task group has been orphaned.
   */

  pgrp = task_getgroup(ppid);
  if (!pgrp)
    {
      /* Set the task group ID to an invalid group ID.  The dead parent
       * task group ID could get reused some time in the future.
       */

      chgrp->tg_ppid = INVALID_PROCESS_ID;
      return;
    }

  /* Save the exit status now if this is the main thread of the task group
   * that is exiting. Only the exiting main task of a task group carries
   * interpretable exit  Check if this is the main task that is exiting.
   */

#ifndef CONFIG_DISABLE_PTHREAD
  if ((ctcb->flags & TCB_FLAG_TTYPE_MASK) != TCB_FLAG_TTYPE_PTHREAD)
#endif
    {
      nxtask_exitstatus(pgrp, status);
    }

  /* But only the final exiting thread in a task group, whatever it is,
   * should generate SIGCHLD.
   */

  if (sq_is_singular(&chgrp->tg_members))
    {
      /* Mark that all of the threads in the task group have exited */

      nxtask_groupexit(pgrp);

      /* Create the siginfo structure.  We don't actually know the cause.
       * That is a bug. Let's just say that the child task just exited
       * for now.
       */

      info.si_signo           = SIGCHLD;
      info.si_code            = CLD_EXITED;
      info.si_errno           = OK;
      info.si_value.sival_ptr = NULL;
      info.si_pid             = chgrp->tg_pid;
      info.si_status          = pgrp->tg_exitcode;

      /* Send the signal to one thread in the group */

      group_signal(pgrp, &info);
    }
}

#else /* HAVE_GROUP_MEMBERS */

static inline void nxtask_sigchild(FAR struct tcb_s *ptcb,
                                   FAR struct tcb_s *ctcb, int status)
{
  siginfo_t info;

  /* If task groups are not supported then we will report SIGCHLD when the
   * task exits.  Unfortunately, there could still be threads in the group
   * that are still running.
   */

#ifndef CONFIG_DISABLE_PTHREAD
  if ((ctcb->flags & TCB_FLAG_TTYPE_MASK) != TCB_FLAG_TTYPE_PTHREAD)
#endif
    {
#ifdef CONFIG_SCHED_CHILD_STATUS
      /* Save the exit status now of the main thread */

      nxtask_exitstatus(ptcb->group, status);

#else /* CONFIG_SCHED_CHILD_STATUS */
      /* Exit status is not retained.  Just decrement the number of
       * children from this parent.
       */

      DEBUGASSERT(ptcb->group != NULL && ptcb->group->tg_nchildren > 0);
      ptcb->group->tg_nchildren--;

#endif /* CONFIG_SCHED_CHILD_STATUS */

      /* Create the siginfo structure.  We don't actually know the cause.
       * That is a bug. Let's just say that the child task just exited
       * for now.
       */

      info.si_signo           = SIGCHLD;
      info.si_code            = CLD_EXITED;
      info.si_errno           = OK;
      info.si_value.sival_ptr = NULL;
      info.si_pid             = ctcb->group->tg_pid;
      info.si_status          = status;

      /* Send the signal.  We need to use this internal interface so that we
       * can provide the correct si_code value with the signal.
       */

      nxsig_tcbdispatch(ptcb, &info);
    }
}

#endif /* HAVE_GROUP_MEMBERS */
#else /* CONFIG_SCHED_HAVE_PARENT */

#  define nxtask_sigchild(x,ctcb,status)

#endif /* CONFIG_SCHED_HAVE_PARENT */

/****************************************************************************
 * Name: nxtask_signalparent
 *
 * Description:
 *   Send the SIGCHLD signal to the parent task group
 *
 ****************************************************************************/

#ifdef CONFIG_SCHED_HAVE_PARENT
static inline void nxtask_signalparent(FAR struct tcb_s *ctcb, int status)
{
#ifdef HAVE_GROUP_MEMBERS
  DEBUGASSERT(ctcb && ctcb->group);

  /* Send SIGCHLD to all members of the parent's task group */

  nxtask_sigchild(ctcb->group->tg_ppid, ctcb, status);
#else
  FAR struct tcb_s *ptcb;

  /* Get the TCB of the receiving, parent task.  We do this early to
   * handle multiple calls to nxtask_signalparent.
   */

  ptcb = nxsched_get_tcb(ctcb->group->tg_ppid);
  if (ptcb == NULL)
    {
      /* The parent no longer exists... bail */

      return;
    }

  /* Send SIGCHLD to all members of the parent's task group.  NOTE that the
   * SIGCHLD signal is only sent once either (1) if this is the final thread
   * of the task group that is exiting (HAVE_GROUP_MEMBERS) or (2) if the
   * main thread of the group is exiting (!HAVE_GROUP_MEMBERS).
   */

  nxtask_sigchild(ptcb, ctcb, status);
#endif
}
#else
#  define nxtask_signalparent(ctcb,status)
#endif

/****************************************************************************
 * Name: nxtask_exitwakeup
 *
 * Description:
 *   Wakeup any tasks waiting for this task to exit
 *
 ****************************************************************************/

#if defined(CONFIG_SCHED_WAITPID) && !defined(CONFIG_SCHED_HAVE_PARENT)
static inline void nxtask_exitwakeup(FAR struct tcb_s *tcb, int status)
{
  FAR struct task_group_s *group = tcb->group;
  int semvalue;

  /* Have we already left the group? */

  if (group)
    {
      /* Only tasks (and kernel threads) return valid status.  Record the
       * exit status when the task exists.  The group, however, may still
       * be executing.
       */

#ifndef CONFIG_DISABLE_PTHREAD
      if ((tcb->flags & TCB_FLAG_TTYPE_MASK) != TCB_FLAG_TTYPE_PTHREAD)
#endif
        {
          /* Report the exit status.  We do not nullify tg_statloc here
           * because we want to prevent other tasks from registering for
           * the return status.  There is only one task per task group,
           * there for, this logic should execute exactly once in the
           * lifetime of the task group.
           *
           * "If more than one thread is suspended in waitpid() awaiting
           *  termination of the same process, exactly one thread will
           *  return the process status at the time of the target process
           *  termination."
           *
           *  Hmmm.. what do we return to the others?
           */

          if (group->tg_statloc != NULL)
            {
              *group->tg_statloc = status << 8;
            }
        }

      /* Is this the last thread in the group? */

#ifndef CONFIG_DISABLE_PTHREAD
      if (sq_is_singular(&group->tg_members))
#endif
        {
          /* Yes.. Wakeup any tasks waiting for this task to exit */

          group->tg_statloc   = NULL;
          group->tg_waitflags = 0;

          nxsem_get_value(&group->tg_exitsem, &semvalue);
          while (semvalue < 0)
            {
              /* Wake up the thread */

              nxsem_post(&group->tg_exitsem);
              nxsem_get_value(&group->tg_exitsem, &semvalue);
            }
        }
    }
}
#else
#  define nxtask_exitwakeup(tcb, status)
#endif

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: nxtask_exithook
 *
 * Description:
 *   This function implements some of the internal logic of exit() and
 *   task_delete().  This function performs some clean-up and other actions
 *   required when a task exits:
 *
 *   - All open streams are flushed and closed.
 *   - All functions registered with atexit() and on_exit() are called, in
 *     the reverse order of their registration.
 *
 *   When called from exit(), the tcb still resides at the head of the ready-
 *   to-run list.  The following logic is safe because we will not be
 *   returning from the exit() call.
 *
 *   When called from nxtask_terminate() we are operating on a different
 *   thread; on the thread that called task_delete().  In this case,
 *   task_delete will have already removed the tcb from the ready-to-run
 *   list to prevent any further action on this task.
 *
 ****************************************************************************/

void nxtask_exithook(FAR struct tcb_s *tcb, int status)
{
#ifdef CONFIG_SCHED_DUMP_LEAK
  struct mm_memdump_s dump =
  {
    tcb->pid,
#  if CONFIG_MM_BACKTRACE >= 0
    0,
    ULONG_MAX
#  endif
  };
#endif

  /* Under certain conditions, nxtask_exithook() can be called multiple
   * times.  A bit in the TCB was set the first time this function was
   * called.  If that bit is set, then just exit doing nothing more..
   */

  DEBUGASSERT((tcb->flags & TCB_FLAG_EXIT_PROCESSING) != 0);

  nxsched_dumponexit();

  /* If the task was terminated by another task, it may be in an unknown
   * state.  Make some feeble effort to recover the state.
   */

  nxtask_recover(tcb);

  /* Disable the scheduling function to prevent other tasks from
   * being deleted after they are awakened
   */

  sched_lock();

  /* Send the SIGCHLD signal to the parent task group */

  nxtask_signalparent(tcb, status);

  /* Wakeup any tasks waiting for this task to exit */

  nxtask_exitwakeup(tcb, status);

  sched_unlock();

  /* Leave the task group.  Perhaps discarding any un-reaped child
   * status (no zombies here!)
   */

  group_leave(tcb);

  /* Deallocate anything left in the TCB's queues */

  nxsig_cleanup(tcb); /* Deallocate Signal lists */

#ifdef CONFIG_SCHED_DUMP_LEAK
  if ((tcb->flags & TCB_FLAG_TTYPE_MASK) == TCB_FLAG_TTYPE_KERNEL)
    {
      kmm_memdump(&dump);
    }
  else
    {
      umm_memdump(&dump);
    }
#endif
}
